pub mod api;
pub mod core;
mod server;
pub mod utils;

use std::sync::Arc;

use crate::utils::ruma_wrapper::{Ruma, RumaResponse};
use api::{client, server as server_api};
use axum::{
    extract::FromRequestParts,
    response::IntoResponse,
    routing::{on, MethodFilter},
};
use http::Method;
use lazy_static::lazy_static;
use redb::Database;
use ruma::api::IncomingRequest;
pub use server::SERVER;
use tracing::{info, info_span, Span};
pub use utils::error::{Error, Result};

use axum::{
    extract::{MatchedPath, Request},
    Router,
};
use std::{future::Future, time::Duration};
use tower_http::{
    cors::{self, CorsLayer},
    trace::TraceLayer,
};

lazy_static! {
    pub static ref DATABASE: Arc<Database> = {
        let db = Database::create("lemetrix_server.redb").unwrap();
        Arc::new(db)
    };
}

pub const DEVICE_ID_LENGTH: usize = 32;
pub const TOKEN_LENGTH: usize = 32;
pub const RANDOM_USER_ID_LENGTH: usize = 32;
pub const SALT_LENGTH: usize = 16;

pub async fn start_server(server_name: &Option<String>) {
    let app = Router::new()
        .ruma_route(client::room::create_room_route)
        .ruma_route(client::key::get_device_route)
        .ruma_route(client::filter::create_filter_route)
        .ruma_route(client::filter::get_filter_route)
        .ruma_route(client::profile::get_profile_route)
        .ruma_route(client::push::get_pushrules_all_route)
        .ruma_route(client::capability::get_capabilities_route)
        .ruma_route(client::user::get_register_available_route)
        .ruma_route(client::user::get_login_types_route)
        .ruma_route(server_api::get_server_version_route)
        .ruma_route(server_api::well_known::well_known_server)
        .ruma_route(client::key::upload_keys_route)
        .ruma_route(client::sync::sync_events_route)
        .ruma_route(client::config::get_global_account_data_route)
        .ruma_route(client::well_known::well_known_client)
        .ruma_route(client::well_known::get_supported_versions_route)
        .ruma_route(client::user::register_route)
        .ruma_route(client::message::send_message_event_route)
        .ruma_route(client::user::login_route);
    let app = app
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(|request: &Request<_>| {
                    // Log the matched route's path (with placeholders not filled in).
                    // Use request.uri() or OriginalUri if you want the real path.
                    let matched_path = request
                        .extensions()
                        .get::<MatchedPath>()
                        .map(MatchedPath::as_str);
                    let _path_and_query = request
                        .uri()
                        .path_and_query()
                        .map(|pq| pq.as_str())
                        .unwrap_or("");
                    info_span!(
                        "http_request",
                        method = ?request.method(),
                        matched_path,
                        some_other_field = tracing::field::Empty,
                    )
                })
                .on_request(|_request: &Request<_>, _span: &Span| {
                    info!("{}", _request.uri());
                }),
        )
        .layer(cors_layer());
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

fn cors_layer() -> CorsLayer {
    CorsLayer::new()
        .allow_private_network(true)
        // .allow_credentials(true)
        .allow_origin(cors::Any)
        .allow_methods(cors::Any)
        .allow_headers(cors::Any)
        .max_age(Duration::from_secs(86400))
}

trait RouterExt {
    fn ruma_route<H, T>(self, handler: H) -> Self
    where
        H: RumaHandler<T>,
        T: 'static;
}

impl RouterExt for Router {
    fn ruma_route<H, T>(self, handler: H) -> Self
    where
        H: RumaHandler<T>,
        T: 'static,
    {
        handler.add_to_router(self)
    }
}

pub trait RumaHandler<T> {
    // Can't transform to a handler without boxing or relying on the nightly-only
    // impl-trait-in-traits feature. Moving a small amount of extra logic into the trait
    // allows bypassing both.
    fn add_to_router(self, router: Router) -> Router;
}

macro_rules! impl_ruma_handler {
    ( $($ty:ident),* $(,)? ) => {
        #[axum::async_trait]
        #[allow(non_snake_case)]
        impl<Req, E, F, Fut, $($ty,)*> RumaHandler<($($ty,)* Ruma<Req>,)> for F
        where
            Req: IncomingRequest + Send + 'static,
            F: FnOnce($($ty,)* Ruma<Req>) -> Fut + Clone + Send + 'static,
            Fut: Future<Output = Result<Req::OutgoingResponse, E>>
                + Send,
            E: IntoResponse,
            $( $ty: FromRequestParts<()> + Send + 'static, )*
        {
            fn add_to_router(self, mut router: Router) -> Router {
                let meta = Req::METADATA;
                let method_filter = method_to_filter(meta.method);

                for path in meta.history.all_paths() {
                    let handler = self.clone();

                    router = router.route(path, on(method_filter, |$( $ty: $ty, )* req| async move {
                        handler($($ty,)* req).await.map(RumaResponse)
                    }))
                }

                router
            }
        }
    };
}

impl_ruma_handler!();
impl_ruma_handler!(T1);
impl_ruma_handler!(T1, T2);
impl_ruma_handler!(T1, T2, T3);
impl_ruma_handler!(T1, T2, T3, T4);
impl_ruma_handler!(T1, T2, T3, T4, T5);
impl_ruma_handler!(T1, T2, T3, T4, T5, T6);
impl_ruma_handler!(T1, T2, T3, T4, T5, T6, T7);
impl_ruma_handler!(T1, T2, T3, T4, T5, T6, T7, T8);

fn method_to_filter(method: Method) -> MethodFilter {
    match method {
        Method::DELETE => MethodFilter::DELETE,
        Method::GET => MethodFilter::GET,
        Method::HEAD => MethodFilter::HEAD,
        Method::OPTIONS => MethodFilter::OPTIONS,
        Method::PATCH => MethodFilter::PATCH,
        Method::POST => MethodFilter::POST,
        Method::PUT => MethodFilter::PUT,
        Method::TRACE => MethodFilter::TRACE,
        m => panic!("Unsupported HTTP method: {m:?}"),
    }
}
